/**
 * QUANSHI.com Inc.
 * Copyright (c) 2016-2017 All Rights Reserved.
 */
package com.quanshi.scheduler.job.biz.service.impl;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.Resource;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.quartz.CronExpression;
import org.quartz.SchedulerException;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;

import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.quanshi.scheduler.core.base.RegistryConfig;
import com.quanshi.scheduler.core.enums.ExecutorBlockStrategyEnum;
import com.quanshi.scheduler.core.enums.GlueTypeEnum;
import com.quanshi.scheduler.job.biz.enums.ExecutorFailStrategyEnum;
import com.quanshi.scheduler.job.biz.enums.ExecutorRouteStrategyEnum;
import com.quanshi.scheduler.job.biz.quartz.JobDynamicScheduler;
import com.quanshi.scheduler.job.biz.quartz.monitor.JobRegistryMonitor;
import com.quanshi.scheduler.job.biz.service.BaseService;
import com.quanshi.scheduler.job.biz.service.JobService;
import com.quanshi.scheduler.job.biz.service.TriggerGroupService;
import com.quanshi.scheduler.job.biz.service.TriggerInfoService;
import com.quanshi.scheduler.job.biz.service.TriggerLogService;
import com.quanshi.scheduler.job.biz.service.TriggerLogglueService;
import com.quanshi.scheduler.job.biz.service.TriggerRegistryService;
import com.quanshi.scheduler.job.dal.entity.TriggerGroup;
import com.quanshi.scheduler.job.dal.entity.TriggerInfo;

/**
 * 
 * @author yanxiang.huang 2017-07-10 10:18:28
 */
@Service
public class JobServiceImpl extends BaseService implements JobService
{

    @Resource
    private TriggerGroupService triggerGroupService;

    @Resource
    private TriggerInfoService triggerInfoService;
    
    @Resource
    private TriggerLogService triggerLogService;
    
    @Resource
    private TriggerRegistryService triggerRegistryService;
    
    @Resource
    private TriggerLogglueService triggerLogglueService;

    @Override
    public boolean add( TriggerInfo jobInfo )
    {
        Assert.notNull( jobInfo, "job info can't be null." );
        TriggerGroup group = triggerGroupService.get( jobInfo.getJobGroup() );
        if ( group == null )
        {
            logger.info( "executor can't be null." );
            return false;
        }
        if ( !CronExpression.isValidExpression( jobInfo.getJobCron() ) )
        {
            logger.info( "cron expression invalid. cron:{}.", jobInfo.getJobCron() );
            return false;
        }
        if ( StringUtils.isBlank( jobInfo.getJobDesc() ) )
        {
            logger.info( "job desc can't be empty." );
            return false;
        }
        if ( StringUtils.isBlank( jobInfo.getAuthor() ) )
        {
            logger.info( "job author can't be empty." );
            return false;
        }
        if ( ExecutorRouteStrategyEnum.match( jobInfo.getExecutorRouteStrategy(), null ) == null )
        {
            logger.info( "executor route strategy invalid. strategy:{}.", jobInfo.getExecutorRouteStrategy() );
            return false;
        }
        if ( ExecutorBlockStrategyEnum.match( jobInfo.getExecutorBlockStrategy(), null ) == null )
        {
            logger.info( "executor block strategy invalid. strategy:{}.", jobInfo.getExecutorBlockStrategy() );
            return false;
        }
        if ( ExecutorFailStrategyEnum.match( jobInfo.getExecutorFailStrategy(), null ) == null )
        {
            logger.info( "executor fail strategy invalid. strategy:{}.", jobInfo.getExecutorFailStrategy() );
            return false;
        }
        if ( GlueTypeEnum.match( jobInfo.getGlueType() ) == null )
        {
            logger.info( "glue type invalid. type:{}.", jobInfo.getGlueType() );
            return false;
        }
        if ( GlueTypeEnum.BEAN == GlueTypeEnum.match( jobInfo.getGlueType() )
                && StringUtils.isBlank( jobInfo.getExecutorHandler() ) )
        {
            logger.info( "bean model must provide a bean handler." );
            return false;
        }

        if ( GlueTypeEnum.GLUE_SHELL == GlueTypeEnum.match( jobInfo.getGlueType() ) && jobInfo.getGlueSource() != null )
        {
            jobInfo.setGlueSource( jobInfo.getGlueSource().replaceAll( "\r", "" ) );
        }

        // childJobKey valid
        if ( StringUtils.isNotBlank( jobInfo.getChildJobkey() ) )
        {
            String[] childJobKeys = jobInfo.getChildJobkey().split( "," );
            for ( String childJobKeyItem : childJobKeys )
            {
                String[] childJobKeyArr = childJobKeyItem.split( "_" );
                if ( childJobKeyArr.length != 2 )
                {
                    logger.info( "invalid child key. key:{}.", childJobKeyItem );
                    return false;
                }
                TriggerInfo childJobInfo = triggerInfoService.get( NumberUtils.toLong( childJobKeyArr[1] ) );
                if ( childJobInfo == null )
                {
                    logger.info( "invalid child key. key:{}.", childJobKeyItem );
                    return false;
                }
            }
        }
        
        // add in db
        boolean res = triggerInfoService.save(jobInfo);
        if (!res) {
            logger.info( "add job to db fail." );
            return false;
        }
        
        // add in quartz
        String qz_group = String.valueOf(jobInfo.getJobGroup());
        String qz_name = String.valueOf(jobInfo.getId());
        try {
            JobDynamicScheduler.addJob(qz_name, qz_group, jobInfo.getJobCron());
            return true;
        } catch (SchedulerException e) {
            logger.error("add job error.", e);
            try {
                triggerInfoService.delete(jobInfo.getId());
                JobDynamicScheduler.removeJob(qz_name, qz_group);
            } catch (SchedulerException e1) {
                logger.error("remove job error.", e1);
            }
        }
        logger.info( "add job fail." );
        return false;
    }

    @Override
    public boolean reschedule( TriggerInfo jobInfo )
    {
        Assert.notNull( jobInfo, "job info can't be null." );
        // valid
        if ( !CronExpression.isValidExpression( jobInfo.getJobCron() ) )
        {
            logger.info( "cron expression invalid. cron:{}.", jobInfo.getJobCron() );
            return false;
        }
        if ( StringUtils.isBlank( jobInfo.getJobDesc() ) )
        {
            logger.info( "job desc can't be empty." );
            return false;
        }
        if ( StringUtils.isBlank( jobInfo.getAuthor() ) )
        {
            logger.info( "job author can't be empty." );
            return false;
        }
        if ( ExecutorRouteStrategyEnum.match( jobInfo.getExecutorRouteStrategy(), null ) == null )
        {
            logger.info( "executor route strategy invalid. strategy:{}.", jobInfo.getExecutorRouteStrategy() );
            return false;
        }
        if ( ExecutorBlockStrategyEnum.match( jobInfo.getExecutorBlockStrategy(), null ) == null )
        {
            logger.info( "executor block strategy invalid. strategy:{}.", jobInfo.getExecutorBlockStrategy() );
            return false;
        }
        if ( ExecutorFailStrategyEnum.match( jobInfo.getExecutorFailStrategy(), null ) == null )
        {
            logger.info( "executor fail strategy invalid. strategy:{}.", jobInfo.getExecutorFailStrategy() );
            return false;
        }
        // childJobKey valid
        if (StringUtils.isNotBlank(jobInfo.getChildJobkey())) {
            String[] childJobKeys = jobInfo.getChildJobkey().split(",");
            for (String childJobKeyItem: childJobKeys) {
                String[] childJobKeyArr = childJobKeyItem.split("_");
                if (childJobKeyArr.length != 2) {
                    logger.info( "invalid child key. key:{}.", childJobKeyItem );
                    return false;
                }
                TriggerInfo childJobInfo = triggerInfoService.get( Long.valueOf(childJobKeyArr[1]) );
                if (childJobInfo==null) {
                    logger.info( "invalid child key. key:{}.", childJobKeyItem );
                    return false;
                }
            }
        }
        // stage job info
        boolean res = triggerInfoService.update(jobInfo);
        if (!res) {
            logger.info( "update job info error." );
            return false;
        }
        // fresh quartz
        String qz_group = String.valueOf(jobInfo.getJobGroup());
        String qz_name = String.valueOf(jobInfo.getId());
        try {
            boolean ret = JobDynamicScheduler.rescheduleJob(qz_group, qz_name, jobInfo.getJobCron());
            return ret;
        } catch (SchedulerException e) {
            logger.error("reschedule job error.", e);
        }
        return false;
    }

    @Override
    public boolean remove( long id )
    {
        TriggerInfo jobInfo = triggerInfoService.get( id );
        if (jobInfo == null) {
            logger.info( "job info can't found by id:{}.", id );
            return false;
        }
        String qz_group = String.valueOf(jobInfo.getJobGroup());
        String qz_name = String.valueOf(jobInfo.getId());
        try
        {
            boolean res = JobDynamicScheduler.removeJob( qz_name, qz_group );
            if (res) {
                triggerInfoService.delete( id );
                triggerLogService.deleteByJobId(id);
                triggerLogglueService.deleteByJobId(id);
            }
            return res;
        }
        catch ( SchedulerException e )
        {
            logger.error("remove job error.", e);
        }
        return false;
    }

    @Override
    public boolean pause( long id )
    {
        TriggerInfo jobInfo = triggerInfoService.get( id );
        if (jobInfo == null) {
            logger.info( "job info can't found by id:{}.", id );
            return false;
        }
        String qz_group = String.valueOf(jobInfo.getJobGroup());
        String qz_name = String.valueOf(jobInfo.getId());
        try
        {
            boolean res = JobDynamicScheduler.pauseJob( qz_name, qz_group );
            return res;
        }
        catch ( SchedulerException e )
        {
            logger.error("pause job error.", e);
        }
        return false;
    }

    @Override
    public boolean resume( long id )
    {
        TriggerInfo jobInfo = triggerInfoService.get( id );
        if (jobInfo == null) {
            logger.info( "job info can't found by id:{}.", id );
            return false;
        }
        String qz_group = String.valueOf(jobInfo.getJobGroup());
        String qz_name = String.valueOf(jobInfo.getId());
        try
        {
            boolean res = JobDynamicScheduler.resumeJob( qz_name, qz_group );
            return res;
        }
        catch ( SchedulerException e )
        {
            logger.error("resume job error.", e);
        }
        return false;
    }

    @Override
    public boolean triggerJob( long id )
    {
        TriggerInfo jobInfo = triggerInfoService.get( id );
        if (jobInfo == null) {
            logger.info( "job info can't found by id:{}.", id );
            return false;
        }
        String qz_group = String.valueOf(jobInfo.getJobGroup());
        String qz_name = String.valueOf(jobInfo.getId());
        try
        {
            boolean res = JobDynamicScheduler.triggerJob( qz_name, qz_group );
            return res;
        }
        catch ( SchedulerException e )
        {
            logger.error("trigger job error.", e);
        }
        return false;
    }

    @Override
    public Map<String, Object> dashboard()
    {
        long jobInfoCount = triggerInfoService.findAllCount();
        long jobLogCount = triggerLogService.findAllCount();
        long jobLogTriggingCount = triggerLogService.findTrggingCount();
        long jobLogTriggerSucCount = triggerLogService.findTriggerSucCount();
        long jobLogTriggerFailCount = triggerLogService.findTriggerFailCount();
        long jobLogRunningCount = triggerLogService.findRunningCount();
        long jobLogHandleSucCount = triggerLogService.findHandleSucCount();
        long jobLogHandleFailCount = triggerLogService.findHandleFailCount();
        Set<String> executorAddressSet = getExecutors();
        int executorCount = executorAddressSet.size();
        
        Map<String, Object> dashboardMap = Maps.newHashMap();
        dashboardMap.put("jobInfoCount", jobInfoCount);
        dashboardMap.put("executorCount", executorCount);
        dashboardMap.put("executors", executorAddressSet);
        
        dashboardMap.put("jobLogCount", jobLogCount);
        dashboardMap.put("jobLogTriggingCount", jobLogTriggingCount);
        dashboardMap.put("jobLogTriggerSucCount", jobLogTriggerSucCount);
        dashboardMap.put("jobLogTriggerFailCount", jobLogTriggerFailCount);
        dashboardMap.put("jobLogRunningCount", jobLogRunningCount);
        dashboardMap.put("jobLogHandleSucCount", jobLogHandleSucCount);
        dashboardMap.put("jobLogHandleFailCount", jobLogHandleFailCount);
        return dashboardMap;
    }

    /**
     * 获取可用执行器地址列表
     *
     * @return
     */
    private Set<String> getExecutors()
    {
        Set<String> executorAddressSet = Sets.newHashSet();
        List<TriggerGroup> groups = triggerGroupService.findAll();
        if ( CollectionUtils.isNotEmpty( groups ) )
        {
            for ( TriggerGroup group : groups )
            {
                List<String> registryList = null;
                if ( group.getAddressType() == 0 )
                {
                    registryList = JobRegistryMonitor.discover( RegistryConfig.RegistType.EXECUTOR.name(),
                            group.getAppName() );
                }
                else
                {
                    if ( StringUtils.isNotBlank( group.getAddressList() ) )
                    {
                        registryList = Arrays.asList( group.getAddressList().split( "," ) );
                    }
                }
                if ( CollectionUtils.isNotEmpty( registryList ) )
                {
                    executorAddressSet.addAll( registryList );
                }
            }
        }
        return executorAddressSet;
    }

    
}
